//! Buffer cache.
//!
//! The buffer cache is a linked list of buf structures holding cached copies of disk block contents.
//! Caching disk blocks in memory reduces the number of disk reads and also provides a synchronization
//! point for disk blocks used by multiple processes.
//!
//! Interface:
//! * To get a buffer for a particular disk block, call read.
//! * After changing buffer data, call write to write it to disk.
//! * When done with the buffer, call release.
//! * Do not use the buffer after calling release.
//! * Only one process at a time can use a buffer, so do not keep them longer than necessary.
const Buffer = @This();

node: List.Node = .{},
sleep: os.Lock.Sleep = .init("buffer"),
refcnt: u32 = 0,

/// has data been read from disk?
valid: bool = false,
/// does disk "own" buf?
own: bool = false,
block: Block = .empty,
data: [fs.BSIZE]u8 align(@alignOf(usize)) = @splat(0),

var lock: os.Lock.Spin = .init("buffer");
var bufs: [fs.NBUF]Buffer = @splat(.{});

/// Linked list of all buffers. Sorted by how recently the buffer was used. list.first is most recent,
/// list.last is least.
var list: List = .{};

pub fn init() void {
    // Create linked list of buffers
    for (&bufs) |*buf| {
        list.append(&buf.node);
    }
}

/// Look through buffer cache for block. If not found, allocate a buffer. In either case, return locked
/// buffer.
fn get(block: Block) *Buffer {
    lock.acquire();

    // Is the block already cached?
    var it = list.first;
    while (it) |node| : (it = node.next) {
        const b: *Buffer = @fieldParentPtr("node", node);
        if (b.block == block) {
            b.refcnt += 1;

            lock.release();
            b.sleep.acquire();
            return b;
        }
    }

    // Not cached. Recycle the least recently used (LRU) unused buffer.
    it = list.last;
    while (it) |node| : (it = node.prev) {
        const b: *Buffer = @fieldParentPtr("node", node);
        if (b.refcnt == 0) {
            b.block = block;
            b.valid = false;
            b.refcnt = 1;

            lock.release();
            b.sleep.acquire();
            return b;
        }
    }

    @panic("get: no buffers");
}

/// Return a locked buf with the contents of the indicated block.
pub fn read(block: Block) *Buffer {
    var b: *Buffer = .get(block);
    if (!b.valid) {
        disk.read(b);
        b.valid = true;
    }
    return b;
}

/// Write b's contents to disk.  Must be locked.
pub fn write(buf: *Buffer) void {
    if (!buf.sleep.holding()) {
        @panic("Buffer.write");
    }
    disk.write(buf);
}

/// Release a locked buffer. Move to the head of the most-recently-used list.
pub fn release(buf: *Buffer) void {
    if (!buf.sleep.holding()) {
        @panic("Buffer.release");
    }

    buf.sleep.release();

    lock.acquire();
    defer lock.release();

    buf.refcnt -= 1;
    // no one is waiting for it.
    if (buf.refcnt == 0) {
        list.remove(&buf.node);
        list.prepend(&buf.node);
    }
}

pub fn pin(buf: *Buffer) void {
    lock.acquire();
    defer lock.release();
    buf.refcnt += 1;
}

pub fn unpin(buf: *Buffer) void {
    lock.acquire();
    defer lock.release();
    buf.refcnt -= 1;
}

const std = @import("std");
const List = std.DoublyLinkedList;
const fs = @import("../fs.zig");
const Block = fs.Block;
const os = @import("../os.zig");
const disk = os.driver.virtio.disk;
